结构体
Rust 中的结构体是一种自定义数据类型,可以将多个不同类型的数据组成一个结合体。
定义
关键字 struct
用于定义结构体。结构体可以包含字段 (成员变量),每个字段都有自己的名称和类型。结构体名称通常以大写字母开头,以区分于变量和函数名称,也要能够反应出自身数据组合的意义。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
在创建结构体变量时需要为每个字段赋予具体的值
let user1 = User {
active: true,
username: String::from("ymfc"),
email: String::from("xxxxxx@gmail.com"),
sign_in_count: 1,
};
创建后可通过点号来访问特定字段
println!("{}", user1.email);
如何要修改某个字段的值,需将结构体变量声明为可变,加上 mut 就行。
注意:若结构体变量声明为可变,那么所有字段都是可变的。但 Rust 不允许单独声明某一部分字段为可变!
在函数中返回结构体
定义一个返回结构体的函数
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
当函数的参数名与结构体中字段名称相同时可简化写法
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
结构体更新语法
在创建结构变量中,若只需要修改一小部分字段,其余字段的值与旧实例相同,可以使用更新语法来快速创建。
// 结构体更新语法
let user2 = User {
email: String::from("123456789@qq.com"),
..user1
};
针对上述有几点需要说明:
- 上述代码发生了移动;
- 若后续去访问 user1.username 时编译会报错,在创建完 user2 后,user1 中 username 就不能再访问了;
- 其它字段可访问,因为它们实现了 Copy trait;
- 代码中 ..user1 必须得放置在结构体初始化代码的最后;
println!("{}", user2.email);
// println!("{}", user1.username); // 发生了移动,String 没实现 Copy trait,所以 user1 中此字段失效
println!("{}", user1.active); // 其它的可以,因为 bool 实现 Copy trait
元组结构体
元组结构体是使用类似元组的方式来定义,这类结构体无须在声明它时对其字段进行命名。
// 元组结构体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
// 使用元组结构体
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
单元结构体
单元结构体也为空结构体,意为没有包含任何字段的结构体,单元结构体在什么情况下会用到,当你想要在某些类型上实现一个 trait,却不需要在该类型中存储任何数据时,单元结构体就可以发挥相应的作用。
// 定义单元结构体
struct AlwaysEqual;
// 使用
let subject = AlwaysEqual;
单元结构体不需要花括号或圆括号。我们可以出于测试的目的,为这个类型实现某些特殊的行为,让它的实例等于任何类型的任何实例,而这个行为的实现不会依赖任何数据!
使用结构体示例程序
多种方式编写一个计算长方形面积的程序,相关说明见代码注释。
// 注解,为结构体启用调试输出
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
// 简单方式计算
let width1 = 30;
let height1 = 50;
println!("The area the rectangle is {} square pixels.", area(width1, height1));
// 使用元组传参方式
let rect1 = (30, 50);
println!("The area the rectangle is {} square pixels.", area1(rect1));
// 使用结构体引用传参方式
let rect2 = Rectangle {
width: 30,
height: 50,
};
println!("The area the rectangle is {} square pixels.", area2(&rect2));
// 格式化结构体数据,使用 Debug 格式化输出
// Debug 是另一种格式化 trait
println!("rect2 is {:?}", rect2);
println!("rect2 is {:#?}", rect2);
// 使用 Debug 格式打印的另一种方法是用 dbg! 宏
// 它会获得表达式的所有权
// 打印出宏调用时的文件名称、代码行号及表达式的结果值,并将结果值的所有权返回
let scale = 2;
let rect3 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
// dbg! 宏会将内容打印到标准错误流 stderr
// println! 宏则将其打印到标准输出流 stdout
dbg!(&rect3);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
fn area1(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
fn area2(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
输出
The area the rectangle is 1500 square pixels.
The area the rectangle is 1500 square pixels.
The area the rectangle is 1500 square pixels.
rect2 is Rectangle { width: 30, height: 50 }
rect2 is Rectangle {
width: 30,
height: 50,
}
[src/main.rs:27:16] 30 * scale = 60
[src/main.rs:30:5] &rect3 = Rectangle {
width: 60,
height: 50,
}
方法
方法也是用 fn 关键字及一个名称来声明,可以拥有参数和返回值,与函数依然是两个不同的概念,因为方法总是被定义在结构体 (或者枚举类型、trait 对象) 的上下文中,并且方法的第一个参数永远都是 self,用于指代调用该方法的结构体实例。
定义
定义方法需要使用 impl 关键字
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn width(&self) -> bool {
self.width > 0
}
}
impl 块中的内容都会关联至 Rectangle 类型,调用时用点号
// 方法
let rect4 = Rectangle {
width: 30,
height: 50,
};
println!("The area of the rectangle is {} square pixels.", rect4.area());
if rect4.width() {
println!("The rectangle has a nonzero width; it is {}", rect4.width);
}
如果要修改值,需要将不可变引用改为可变引用 &mut self
,当然,如果只是读取数据,最好还是不要这么做。
方法的名称可以与结构体中字段名称相同,这种通常用于返回字段的值,而不做其它复杂的操作,类似的方法也被称为访问器。
Rust 中没有提供类似 -> 运算符,但设计了一种名为自动引用与解引用的功能作为替代,方法调用是 Rust 中少数几个拥有这种行为的地方之一。当使用 object.something() 调用方法时,Rust 会自动为调用者 object 添加 &、&mut 或 *,以使其能够符合方法的签名,以下是等价的
p1.distance(&p2);
(&p1).distance(&p2);
带有更多参数的方法
来看一个更多参数的方法
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
调用
// 调用带有更多参数的方法
let rect5 = Rectangle {
width: 30,
height: 50,
};
let rect6 = Rectangle {
width: 10,
height: 45,
};
let rect7 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect5 hold rect6? {}", rect5.can_hold(&rect6));
println!("Can rect5 hold rect7? {}", rect5.can_hold(&rect7));
关联函数
所有定义在 impl 块中的函数都被称为关联函数,可以在 impl 块中定义没有将 self 作为第一个参数的函数,当然,此函数也不能称为方法了,它不会作用于某个具体的结构体实例,String::from
就是这样的一个函数。
关联函数常常被用作构造器来返回一个结构体实例的新实例,虽然这些函数常常被命名为 new,但 new 并不是一个内置于语言本身中的特殊名称。
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
返回类型及函数体中的 Self 关键字是一个别名,它指向了 impl 关键字后面的类型,也就是 Rectangle。
调用关联函数
// 调用关联函数
let sq = Rectangle::square(3);
println!("sq width is: {}", sq.width);
总结
结构体可以让你创建有意义的自定义类型。通过使用结构体,可以将相关联的数据组合起来,并为每个数据赋予名字,从而使用代码变得更加清晰。在 impl 块中,可以定义那些与类型相关联的函数,而方法作为一种关联函数,可以为结构体的实例指定行为,但结构体不是创建自定义类型的唯一方法,例如枚举。